/**
 * 
 */
package main;

import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

import util.ParserHandler;
import util.Serializer;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;

/**
 * Cette classe contient la méthode principale du programme. Elle prend un
 * fichier XML en paramètre ainsi que plusieurs arguments possibles. Chaque
 * classe mentionnée dans le fichier XML sera modifiée de telle sorte qu'elle 
 * implémente une interface ISerializable et deux méthodes: serialize et
 * deserialize. Une classe Dispatcher sera créée dans la foulée.
 * 
 * @author Pratik Bhavsar, Fabrice Reynaud.
 * 
 */
public class Generator {

	public static boolean verbose = false;
	
	public static boolean stats = false;
	public static int numberOfClasses = 0;
	public static int maxHierarchy = 0;
	public static int maxField = 0;	
	
	public static boolean length = false;
	public static int nbString = 0;
	

	// -1 sera utilis� dans le cas de inlineall.
	public static int inline = 0;

	static ClassPool pool = ClassPool.getDefault();

	static ArrayList<CtClass> classes = null;

	static CtClass iSeria = pool.makeInterface("util.LRSerializable");

	/**
	 * 
	 * @param args
	 */
	public static void main(String[] args) {

		int argSize = args.length;
		int i = 0;

		if (argSize == 0) {
			System.out.println("usage: java -jar serilization_generator" +
					" [xmlfile] -verbose -stats -length");

			return;
		}

		boolean xmlPresent = false;

		while (i < argSize) {

			if (args[i].equals("-verbose"))
				verbose = true;
			else if (args[i].equals("-stats"))
				stats = true;
			else if (args[i].equals("-length"))
				length = true;
			else if (args[i].equals("-inline"))
				try {
					inline = Integer.parseInt(args[++i]);
				} catch (NumberFormatException e) {
					System.out.println("Veuillez spécifier un nombre correct"
							+ " pour l'argument -inline");
					return;
				}
			else if (args[i].equals("-inlineall"))
				inline = -1;
			else if (args[i].contains(".xml")) {
				if (!parseXMLFile(args[i])) {
					System.out.println("Le fichier .xml passé en "
							+ "paramètre est soit manquant, soit incorrect.");
					return;
				}
				xmlPresent = true;
			} else {
				System.out.println("Un des arguments n'est pas valide: "
						+ args[i]);
				return;
			}
			i++;
		}

		if (!xmlPresent) {
			System.out.println("Vous devez au moins passez un nom de fichier"
					+ " XML en paramètre!");
			return;
		}

		createDispatcher();
		modifyClasses();
		writeDispatcherBody();
		
		if(stats) {
			System.out.println("+ Stats: Hiérarchie maximale = " + maxHierarchy);
			System.out.println("+ Stats: Nombre de champs max = " + maxField);
		}
	}

	/**
	 * Cette méthode parse le fichier XML fournit en paramètre lors de
	 * l'éxecution et initialise la liste des classes à traiter.
	 * 
	 * @param path
	 *            le chemin du fichier XML passé en paramètre.
	 * @return false si la tentaive de parsing a échoué, true sinon.
	 */
	private static boolean parseXMLFile(String path) {
		SAXParserFactory parserFact = SAXParserFactory.newInstance();
		XMLReader xmlreader = null;

		try {
			SAXParser parser = parserFact.newSAXParser();
			xmlreader = parser.getXMLReader();
			xmlreader.setContentHandler(new ParserHandler());
			xmlreader.parse(path);
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		} catch (SAXException e) {
			e.printStackTrace();
			return false;
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
			return false;
		}

		ParserHandler parserHandler = (ParserHandler) xmlreader
				.getContentHandler();
		classes = parserHandler.getCtClasses();
		Serializer.ctclasses = classes;

		return true;
	}

	/**
	 * Cette méthode créée la classe Dispatcher avec la méthode type, mais le
	 * corps de cette méthode sera insérer plus tard.
	 */
	private static void createDispatcher() {

		print("+ Création de la classe Dispatcher.");

		CtClass stringClass = null;
		try {
			stringClass = pool.getCtClass("java.lang.String");
		} catch (NotFoundException e) {
			System.out.println("La classe \"java.lang.String\" est introuvable.");
		}

		CtClass dispatcher = pool.makeClass("Dispatcher");
		CtMethod dispatcher_mtype = null;
		CtClass[] args = new CtClass[1];
		args[0] = stringClass;

		try {
			dispatcher_mtype = new CtMethod(
					pool.getCtClass("java.lang.Object"), "type", args,
					dispatcher);
			dispatcher_mtype.setModifiers(Modifier.PUBLIC + Modifier.STATIC);
			dispatcher.addMethod(dispatcher_mtype);
			dispatcher.writeFile("bin");

		} catch (CannotCompileException e1) {
			e1.printStackTrace();
		} catch (NotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Cette focntion effectue les modifications énoncées plus haut.
	 */
	private static void modifyClasses() {
		for (CtClass c : classes) {
			
			if(stats) {
				CtClass current = c;
				int count = 0;			
				try {
					while((current = current.getSuperclass()) != null)
						count++;
				} catch (NotFoundException e) {}
				if(count > maxHierarchy)
					maxHierarchy = count;
			}
			
			print("\n##### Traitement de la classe " + c.getSimpleName()
					+ " #####");

			print("+ Ajout de l'interface ISerializable.");

			addInterface(c);
			addMethods(c);

			print("+ Ecriture du fichier.");

			try {
				c.writeFile("bin/");
			} catch (CannotCompileException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * Prends une CtClass en argument et lui ajouter l'interface ISerializable.
	 * 
	 * @param c
	 *            la CtClass à modifier.
	 */
	private static void addInterface(CtClass c) {
		boolean hasInterface = false;
		try {
			for (CtClass classe : c.getInterfaces())
				if (classe.getName().equals(iSeria.getName()))
					hasInterface = true;
		} catch (NotFoundException e) {
			System.out.println("La classe " + c.getName() + "n'a pas était"
					+ " trouvée");
		}
		if (!hasInterface)
			c.addInterface(iSeria);
	}

	/**
	 * Prends une CtClass et lui ajoute les méthode serialize et deserialize.
	 * 
	 * @param c  la CtClass à modifier.
	 */
	private static void addMethods(CtClass c) {

		Serializer seri = new Serializer(c);

		seri.createSerializeMethod();
		seri.createDeserializeMethod();

		String bodySerialize = "";
		bodySerialize += "{\n";
		bodySerialize += "\treturn " + seri.serialization + "\"\";\n";
		bodySerialize += "}";

		print("Corps de la methode serialize:\n" + bodySerialize);

		print("+ Ajout de la méthode serialize.");

		try {
			String serialiseMethodString = "public String serialize()"
					+ bodySerialize;
			c.addMethod(CtMethod.make(serialiseMethodString, c));
		} catch (javassist.bytecode.DuplicateMemberException e) {
			System.out.println("La méthode serialize existe déjà pour cette"
					+ " classe.");
		} catch (CannotCompileException e) {
			System.out.println("CannotCompileException " + e.getMessage());
		}

		String bodyDeserialise = "";
		bodyDeserialise += "{\n";
		bodyDeserialise += seri.deserialization;
		bodyDeserialise += "}";

		print("Corps de la methode deserialize:\n" + bodyDeserialise);

		print("+ Ajout de la méthode deserialize.");

		try {
			String deserialiseMethodString = "public static Object deserialize(String serialised)"
					+ bodyDeserialise;
			CtMethod m = CtMethod.make(deserialiseMethodString, c);
			m.setModifiers(Modifier.STATIC + Modifier.PUBLIC);
			c.addMethod(m);
		} catch (javassist.bytecode.DuplicateMemberException e) {
			System.out.println("La méthode serialize existe déjà pour cette"
					+ " classe.");
		} catch (CannotCompileException e) {
			System.out.println("CannotCompileException " + e.getMessage());
		}
	}

	/**
	 * Cette focntion écrit le corps de la méthode type du Dispatcher.
	 */
	private static void writeDispatcherBody() {

		String body = "";
		body += "{\n";
		body += "\tjava.util.StringTokenizer objectString = "
				+ "new java.util.StringTokenizer($1, \";\");\n";
		body += "\tString className = objectString.nextToken();\n";
		body += "\tint classNb = Integer.parseInt(className);\n\n";

		body += "\tswitch(classNb)\n";
		body += "\t{\n";

		int i = 1;
		for (CtClass c : classes) {
			body += "\t\tcase " + i++ + " :\n";
			body += "\t\t\treturn " + c.getName() + ".deserialize($1);\n";

		}
		body += "\t\tdefault:\n";
		body += "\t\t\tSystem.out.println(\"not found\");\n";
		body += "\t\t\treturn null;\n";

		body += "\t}\n";
		body += "}\n";

		print("\nCorps de Dispatcher:\n" + body);

		
		print("+ Ajout du corps de la méthode type de la classe Dispatcher.");
		
		try {
			CtClass dispatcher = pool.getCtClass("Dispatcher");
			dispatcher.defrost();

			CtMethod dispatcher_mtype = dispatcher.getMethod("type",
					"(Ljava/lang/String;)Ljava/lang/Object;");
			dispatcher_mtype.setBody(body);

			dispatcher.writeFile("bin");

		} catch (CannotCompileException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (NotFoundException e) {
			e.printStackTrace();
		}
	}

	private static void print(String s) {
		if (verbose)
			System.out.println(s);
	}
}
